﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Text.RegularExpressions;
using System.IO;
using System.Data;

namespace KingOfCSharp.iHunter.Plugins.BayesianFilter
{
    /// <summary>
    /// Naive Baysiam Spam Filter.
    /// </summary>
    public class BayesianFilterImpl
    {
        #region parameters and variables

        /// <summary>
        /// Constants used in the Bayesian algorithm.
        /// </summary>
        public class ParaList
        {
            // Values in Paul Graham's paper:
            public int GoodTokenWeight = 2;				// 2
            public int MinTokenCount = 0;				// 0
            public int MinCountForInclusion = 5;		// 5
            public double MinScore = 0.01;				// 0.01
            public double MaxScore = 0.99;				// 0.99
            public double LikelySpamScore = 0.9998;		// 0.9998
            public double CertainSpamScore = 0.9999;	// 0.9999
            public int CertainSpamCount = 10;			// 10
            public int InterestingWordCount = 15;		// 15
        }

        private ParaList _para = new ParaList();

        public ParaList Para
        {
            get { return _para; }
            set { _para = value; }
        }

        private Corpus _good;
        private Corpus _bad;
        private SortedDictionary<string, double> _prob;
        private int _ngood;
        private int _nbad;

        private double Max(double a, double b)
        {
            return a > b ? a : b;
        }

        private double Min(double a, double b)
        {
            return a < b ? a : b;
        }

        #endregion

        #region properties

        /// <summary>
        /// A list of words that marked spam
        /// </summary>
        public Corpus Bad
        {
            get { return _bad; }
            set { _bad = value; }
        }

        /// <summary>
        /// A list of words that marked non-spam
        /// </summary>
        public Corpus Good
        {
            get { return _good; }
            set { _good = value; }
        }

        /// <summary>
        /// A list of probabilities that the given word might appear in a spam
        /// </summary>
        public SortedDictionary<string, double> Prob
        {
            get { return _prob; }
            set { _prob = value; }
        }

        #endregion

        #region loading and calculating

        /// <summary>
        /// Initialize the filter based on the texts
        /// </summary>
        /// <param name="goodReader"></param>
        /// <param name="badReader"></param>
        public void Load(TextReader goodReader, TextReader badReader)
        {
            _good = new Corpus(goodReader);
            _bad = new Corpus(badReader);

            CalculateProbabilities();
        }

        /// <summary>
        /// Initialize the filter based on the contents of the corpuses
        /// </summary>
        /// <param name="good"></param>
        /// <param name="bad"></param>
        public void Load(Corpus good, Corpus bad)
        {
            _good = good;
            _bad = bad;

            CalculateProbabilities();
        }

        /// <summary>
        /// Calculate the spam probabilities
        /// </summary>
        private void CalculateProbabilities()
        {
            _prob = new SortedDictionary<string, double>();

            _ngood = _good.Tokens.Count;
            _nbad = _bad.Tokens.Count;
            if (_good == null && _bad == null) return;
            else if (_good == null && _bad != null)
            {
                foreach (string token in _bad.Tokens.Keys)
                {
                    if (!_prob.ContainsKey(token))
                    {
                        CalculateTokenProbability(token);
                    }
                }
            }
            else if (_good != null && _bad == null)
            {
                foreach (string token in _good.Tokens.Keys)
                {
                    CalculateTokenProbability(token);
                }
            }
            else
            {
                foreach (string token in _good.Tokens.Keys)
                {
                    CalculateTokenProbability(token);
                }
                foreach (string token in _bad.Tokens.Keys)
                {
                    if (!_prob.ContainsKey(token))
                    {
                        CalculateTokenProbability(token);
                    }
                }
            }
        }

        /// <summary>
        /// For a given token, calculate the probability that will appear in a spam text
        /// </summary>
        /// <param name="token"></param>
        private void CalculateTokenProbability(string token)
        {
            int g = _good.Tokens.ContainsKey(token) ? _good.Tokens[token] * Para.GoodTokenWeight : 0;
            int b = _bad.Tokens.ContainsKey(token) ? _bad.Tokens[token] : 0;

            if (g + b >= Para.MinCountForInclusion)
            {
                double goodfactor = Min(1, (double)g / (double)_ngood);
                double badfactor = Min(1, (double)b / (double)_nbad);

                double prob = Max(Para.MinScore, Min(Para.MaxScore, badfactor / (goodfactor + badfactor)));

                // special case for Spam-only tokens.
                // .9998 for tokens only found in spam, or .9999 if found more than 10 times
                if (g == 0)
                {
                    prob = (b > Para.CertainSpamCount) ? Para.CertainSpamScore : Para.LikelySpamScore;
                }

                _prob[token] = prob;
            }
        }

        #endregion

        #region serialization

        /// <summary>
        /// Save the probability list to a file, the first line contains good, bad and probability counts.
        /// </summary>
        /// <param name="filePath"></param>
        public void ToFile(string filePath)
        {
            using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
            {
                StreamWriter writer = new StreamWriter(fs);

                writer.WriteLine(String.Format("{0},{1},{2}", _ngood, _nbad, _prob.Count));
                foreach (string key in _prob.Keys)
                {
                    writer.WriteLine(String.Format("{0},{1}", _prob[key].ToString("#.#####"), key));
                }

                writer.Flush();
                fs.Close();
            }
        }

        /// <summary>
        /// Load from a file created with ToFile().
        /// </summary>
        /// <param name="filePath"></param>
        public void FromFile(string filePath)
        {
            _prob = new SortedDictionary<string, double>();
            using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                StreamReader reader = new StreamReader(fs);

                string[] tokens = reader.ReadLine().Split(',');
                if (tokens.Length > 1)
                {
                    _ngood = Convert.ToInt32(tokens[0]);
                    _nbad = Convert.ToInt32(tokens[1]);
                }

                string line = "";
                while ((line = reader.ReadLine()) != null)
                {
                    tokens = line.Split(',');
                    if (tokens.Length > 1)
                    {
                        _prob.Add(tokens[1], Convert.ToDouble(tokens[0]));
                    }
                }

                fs.Close();
            }
        }

        #endregion

        #region filtration

        /// <summary>
        /// Bayesian algorithm, returns the probability that a given text is spam
        /// </summary>
        /// <param name="body"></param>
        /// <returns></returns>
        public double Filtrate(string body)
        {
            SortedList probs = new SortedList();

            // Parse the text and look up each words' spam probability.
            // Keep the list in decending order of "Interestingness"
            Regex re = new Regex(Corpus.TokenPattern, RegexOptions.Compiled);
            Match m = re.Match(body);
            int index = 0;
            while (m.Success)
            {
                string token = m.Groups[1].Value;
                if (_prob.ContainsKey(token))
                {
                    // "interestingness" == how far our score is from 50%.  
                    double prob = _prob[token];
                    string key = (0.5 - Math.Abs(0.5 - prob)).ToString(".00000") + token + index++;
                    probs.Add(key, prob);

                }
                m = m.NextMatch();
            }

            double mult = 1;  // for abc..n
            double comb = 1;  // for (1 - a)(1 - b)(1 - c)..(1-n)
            index = 0;
            foreach (string key in probs.Keys)
            {
                double prob = (double)probs[key];
                mult = mult * prob;
                comb = comb * (1 - prob);
                if (++index > Para.InterestingWordCount) break;
            }
            return mult / (mult + comb);
        }

        #endregion
    }
}
